/*
 * Copyright 2006 - 2007 The MITRE Corporation. All rights reserved.
 * Written by Ayal Spitz
 *
 * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1
 * (the "License"); you may not use this file except in compliance
 * with the License and the following two conditions:
 * 
 * The US Government will not be charged any license fee and/or royalties 
 * related to this software.
 * 
 * Neither name of The MITRE Corporation; nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * You may obtain a copy of the License at
 *
 *     http://www.gnu.org/copyleft/lesser.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

package org.mitre.streamingRSS.xmpp;

import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.jivesoftware.smack.AccountManager;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smackx.muc.MultiUserChat;

/**
 * The Common class is designed to simplify the process of participating in
 * an XMPP interaction. It is the base class of both spitz.ayal.irss.xmpp.Publisher
 * and spitz.ayal.irss.xmpp.Subscriber. This class follows in the footsteps of
 * Ruby on Rails and puts convention over configuration, ie there is an attempt
 * to make smart defaults over long config files.
 * 
 * @author Ayal Spitz
 *
 */
public class Common{
// ### Class Fields ###########################################################
	protected	XMPPConnection				xmppConnection = null;
	private		int							u = 0;
	
	protected 	String						host = null;
	protected 	int							port = -1;
	protected	Hashtable<String, String> 	accountAttr = null;
	protected 	String						userJID = null;
	protected 	String						nickname = null;
	protected	String						rsrcName = null;
	protected	String						defaultChatDomain = null;
	
// ### Class Constructors #####################################################
	/**
	 * This constructor defaults to connecting to the localhost and using the
	 * default port (5222) for connecting to the XMPP server
	 */
	public Common(){ this("localhost"); }
	
	/**
	 * This constructor connects to the host passed in using the default port
	 * (5222) for connecting to the XMPP server
	 * 
	 * @param host					Either the host name or IP address of the 
	 * 								XMPP server. If "localhost" is passed in
	 * 								the constructor will attempt to turn
	 */
	public Common(String host){ this(host,5222); }
	
	/**
	 * This constructor connects to the host and port passed for the XMPP server
	 * 
	 * @param host					Either the host name or IP address of the 
	 * 								XMPP server. If "localhost" is passed in
	 * 								the constructor will attempt to turn
	 * @param port					The port to connect to the XMPP server
	 */
	public Common(String host,int port){
		if (host.equalsIgnoreCase("localhost")){
				try{
					// Attempt to get the actually name of the local host
					host = InetAddress.getLocalHost().getHostName();
				}catch(UnknownHostException e){}
		}

		this.host = host;
		this.port = port;
		this.rsrcName = "iRSS";
		
		this.accountAttr = new Hashtable<String, String>();
	}
	
// ### Class Methods ##########################################################
	/**
	 * This method will attempt to connect to the XMPP server set in the 
	 * constructor.
	 * 
	 * This method starts out by connecting to the XMPP server. It will then
	 * attempt to login. If login fails the connection will be closed. If
	 * newAccount is set to true then the connection will be reopened and then
	 * the method will attempt to create a new account with the username and 
	 * password provided during construction.  If the account creation is 
	 * successful the connection will be closed, reopend, and then another login
	 * attempt will be made. If any part of this fails an XMPPException is thrown.
	 * 
	 * If login is successful the chat domain will be set. If the context chat
	 * domain hasn't been set the chat domain will default to 'conference.' 
	 * service name. If the context chat domain is set and ends with a '.' the
	 * service name will be appended.
	 * 
	 * @param	newAccount		If true will cause the creation of an account on
	 * 							the host if it does not exist. If false then the
	 * 							method will simply attempt to connect
	 * @throws 	XMPPExcetion if any part of the method fails
	 */	
	public void connectAndLogin(String username, String password, 
		boolean newAccount) throws XMPPException{
		
		//System.out.println(username + " " + password + " " + newAccount);
		
		connect(this.host, this.port);
		try{
			// Attempt to login with the username/password/rsrc provided
			xmppConnection.login(username, password, this.rsrcName);
		}catch(XMPPException xmppException){
			if (newAccount){
				XMPPError			xmppError = null;

				// If we can't log in close the connection
				close();

				xmppError = xmppException.getXMPPError();
				if (xmppError != null){
					if (xmppError.getCode() == 409){
						u++;
						connectAndLogin(username + u, password, newAccount);
						return;
					}
				}
							
				// Connect again
				connect(this.host, this.port);
				// Attempt to create a new account
				createAccount(username, password);
				// Close the connection
				close();
				
				// Connect one more time
				connect(this.host, this.port);
				// Login
				//System.out.println(username + " " + password + " " + newAccount);
				xmppConnection.login(username, password, this.rsrcName);
			} else {
				throw xmppException;
			}
		}
		
		updateContext();
	}

	public void connectAndLogin(String username, String password) throws XMPPException{ 
		connectAndLogin(username,password, true);
	}
	
	private void updateContext(){
		this.host = xmppConnection.getHost();
		this.port = xmppConnection.getPort();

		String			user = null;
		user = xmppConnection.getUser();
		user = user.substring(0,user.indexOf("/"));
		this.userJID = user;

		user = user.substring(0,user.indexOf("@"));
		this.nickname = user;
		
		try{
			Iterator<?>			iterator = null;
			
			iterator = MultiUserChat.getServiceNames(xmppConnection).iterator();
			this.defaultChatDomain = ((String)(iterator.next()));
		}catch(Exception e){ e.printStackTrace(); }
	}
	
	/**
	 * This method attempts to connect to the host and port identified as part
	 * of the constructor. This method utilizes the ConnectionConfiguration
	 * to activate stream compression (JEP-138). This method calls the constructor
	 * XMPPConnection.
	 * 
	 * The use of ConnectionConfiguration seems to speed up the connection process
	 * 
	 * @throws XMPPException
	 * 
	 * @see org.jivesoftware.smack.XMPPConnection#Constructor(ConnectionConfiguration)
	 * @see org.jivesoftware.smack.ConnectionConfiguration
	 */
	protected void connect(String host, int port) throws XMPPException{
		// Configure the connection to use compression
		ConnectionConfiguration		connConfig = null;

		// Configure the connection to use the host/port provided
		connConfig = new ConnectionConfiguration(host,port);
		// Configure the connection to use compression (JEP-138)
		connConfig.setCompressionEnabled(true);
		// Configure the connection to use TLS and SASL Authentication
		connConfig.setSecurityMode(ConnectionConfiguration.SecurityMode.required);
		connConfig.setSASLAuthenticationEnabled(true);
		// Configure the connection to use the XMPPConnection.DEBUG_ENABLED flag
		connConfig.setDebuggerEnabled(XMPPConnection.DEBUG_ENABLED);

		xmppConnection = new XMPPConnection(connConfig);
		xmppConnection.connect();
	}

	/**
	 * This method will close the connection to the XMPP server
	 *
	 * @see org.jivesoftware.smack.XMPPConnection#disconnect()
	 */
	public void close(){ xmppConnection.disconnect(); }
		
// ============================================================================
	/**
	 * This method will attempt to create a new account. This method is called
	 * by the loginAndConnect method if the user doesn't appear to be
	 * registered with the XMPP server.
	 * 
	 * This method starts off by check to see if the XMPP server supports
	 * account creation. If it does then it attempts to create a new account.
	 * 
	 * @throws XMPPException
	 */
	private void createAccount(String username, String password) throws XMPPException{
		AccountManager		accountManager = null;
		
		// Get The account manager for this connection
		accountManager = xmppConnection.getAccountManager();
		// Check to make sure that the XMPP server support account creation
		if (accountManager.supportsAccountCreation()){
			try{
				// Try to create the new account
				accountManager.createAccount(username, password);
			}catch(XMPPException xmppException){
				// If an exception is throw it might mean that we have to fill
				// in more informtion to creat an account
				if (xmppException.getXMPPError() != null){
					if (xmppException.getXMPPError().getCode() == 400){
						Map				accountAttr = null;
						
						accountAttr = fillInAccountAttr(accountManager);
						accountManager.createAccount(username, password, 
							accountAttr);
					}
				} else {
					throw xmppException;
				}				
			}
		} else {
			throw new XMPPException("XMPP Server does not support account creation");
		}		
	}
	
	/**
	 * This method attempts to fill in account attributes. This mehtod is
	 * called if the default account creation method isn't successful.
	 * 
	 * @param accountManager	The account manager passed
	 *
	 * @return					Mapping of account keys to account values
	 * 
	 * @throws XMPPException
	 */
	@SuppressWarnings("unchecked")
	private Map fillInAccountAttr(AccountManager accountManager) throws XMPPException{
		Iterator<String>	iterator = null;
		Set<String>			attrSet = null;
		Map					accountAttr = null;
		
		// Attempt to fill in the account attributes from the prefTable
		iterator = ((Iterator<String>)(accountManager.getAccountAttributes()));
		attrSet = new HashSet<String>();
		while(iterator.hasNext()){ attrSet.add(iterator.next()); }
		
		accountAttr = new HashMap<String, String>();
		// Attempt to fill in any account attributes from the context object
		for(String key:attrSet){ accountAttr.put(key, accountAttr.get(key)); }

		// Check to see if there are other attributs we haven't filled in
		//attrSet.removeAll(properties.keySet());
		// If the attribute set isn't empty that means we have some 
		//  account attributes that aren't filled in
		if (!attrSet.isEmpty()){ 
			throw new XMPPException("The XMPP Server requires more attributes then available");
		}
		
		return accountAttr;
	}
	
// ============================================================================
	/**
	 * This method attempts to create a new chat room.
	 * 
	 * @param roomName		The name of the chat room to join
	 * 
	 * @return 				The chat room
	 * 
	 * @throws XMPPException
	 */
	public MultiUserChat createNewChatRoom(String roomName) throws XMPPException{
		MultiUserChat			muc = null;
		String					roomID = null;
		
		roomID = getRoomID(roomName,true);
		muc = new MultiUserChat(xmppConnection,roomID);
		muc.create(this.nickname);
		
		return muc;
	}
	
	/**
	 * This method attempts to join a chat room.
	 * 
	 * @param roomName		The name of the chat room to join
	 * 
	 * @return 				The chat room
	 * 
	 * @throws XMPPException
	 */
	public MultiUserChat joinChatRoom(String roomName) throws XMPPException{
		String					roomID = null;
		MultiUserChat			muc = null;
		
		roomID = getRoomID(roomName,false);
		// Try to get the RoomInfo object for this roomID. If you can't that
		// means there is an issue with the room which causes an XMPPException
		// to be thrown
		MultiUserChat.getRoomInfo(xmppConnection, roomID);

		muc = new MultiUserChat(xmppConnection,roomID);
		muc.join(this.nickname);
		
		return muc;		
	}
	
	/**
	 * This method gets a full room id given a room name. The method first makes
	 * the roomName url safe by encoding it using URLEncoder. Then the method
	 * constructs the full room id drawing on the name of the chat room service.
	 * 
	 * @param roomName		The name of the room to create
	 * 
	 * @return 				The full room name that can be passed in to either
	 * 						the join new chat room or create new chat room
	 * 
	 * @see 				java.net.URLEncoder#encode(String,String)
	 */
	private String getRoomID(String roomName, boolean urlSafe){
		String			safeRoomName = null;
		
		if (urlSafe){
			try{
				safeRoomName = URLEncoder.encode(roomName,"UTF-8");
			}catch(UnsupportedEncodingException e){}
		} else {
			safeRoomName = roomName;
		}
		
		return (safeRoomName + "@" + this.defaultChatDomain);
	}

// ============================================================================
	/**
	 * This method allows the seting of this connections context object
	 * 
	 * @param key		The key to set the value
	 * @param value		The value to set
	 */
	public void putAttr(String key, String value){ accountAttr.put(key, value); }

	/**
	 * This method sets the resoruce of this connections context object
	 * 
	 * @param rsrc		The resource to set
	 */
	public void setResource(String rsrc){ this.rsrcName = rsrc; }
}